# 👉 npm 包管理
# npm 进化史
# npm2 的嵌套地狱
最早期的 npm 版本(npm v2),npm 在安装依赖的时候会将依赖放到 node_modules 文件中;同时,如果某个直接依赖 A ,A 又依赖于其他的依赖包 B,那么依赖 B 会作为间接依赖,安装到依赖 A 的文件夹 node_modules 中,然后可能多个包之间也会有出现同样的依赖递归的。
如果项目一旦过大,那么必然会形成一棵巨大的依赖树,依赖包会出现重复,从而形成嵌套地狱。
# npm3 的扁平依赖
为了解决 npm2 的嵌套地狱,npm3 进行了依赖扁平化优化,将共同的间接依赖也变成直接依赖,
但是,npm 3.x 版本并未完全解决老版本的模块冗余问题。如果上面例子中的依赖 A 和依赖 C 分别依赖 B 的不同版本,npm v3 并不会将两个不同的 B 版本都提出。而是会通过 localeCompare 方法对依赖进行一次排序,最终字典序在前面的 npm 包的底层依赖会被优先提出来,其他版本的 B 依然会嵌套在原来依赖 B 的包下。上面的例子来说,就是依赖版本 Bv2.0 依然嵌套在 C 下。
# npm install 过程做了啥
在我们敲下 npm install 的时候 , npm 会做以下几件事情:
- 检查 config
npm 执行会先读取npm config list
和 npmrc 配置,而 npmrc 是有优先级之分的,后面会展开讲讲。 - 检查是否存在
package-lock.json
- 存在
package-lock.json
,将会检查跟当前 package-lock 里声明的版本是否一致- 版本一致,是否有检查缓存
- 版本不一致 , 对应处理方法跟 npm 版本有关。在最新版本的 npm 中,会检查依赖包兼容版本, 如果版本能兼容,则按照 package-lock 版本安装,反之按照 package.json 版本安装
- 不存在
package-lock.json
,将会执行以下步骤:- 获取依赖包的信息
- 构建依赖树
- 扁平化
- 检查缓存
- 存在
- 检查缓存
- 如果有缓存,将对应缓存解压到 node_modules 下,生成 package-lock
- 如果没有缓存:
- 下载资源包,检查资源包完整性
- 添加到缓存中
- 解压到 node_modules,生成 package-lock
由上面的步骤可以看出,缓存功能在 npm 整个过程中起到非常关键的作用 ,每次安装依赖时都会对对应包创建缓存,那我们如何查看缓存呢?
# npm 缓存
我们可以通过npm config get cache
这个命令获取 npm 缓存在本地的路径,进入该目录后,访问_cacache
目录 , npm 的缓存就放在该目录下。
~ % npm config get cache
/Users/chieminchan/.npm
~ % cd /Users/chieminchan/.npm
~/.npm % ls
_cacache _locks _logs _npx anonymous-cli-metrics.json index-v5
其中 content-v2
是缓存二进制文件 , index-v5
是缓存对应索引 hash.
# npm 是如何利用缓存的
npm 在执行安装时,可以根据 package-lock.json
中存储的 integrity
、version
、name
生成一个唯一的 key 对应到 index-v5 目录下的缓存记录,从而找到依赖包tar
的 hash,然后根据 hash 再去找缓存直接使用。
# 举个例子 🌰
我们在缓存索引目录 index-v5
下搜索一个clean-webpack-plugin-3.0.0.tgz
包测试一下。
grep
命令找到依赖包在index-v5
的位置.npm/_cacache % grep "https://registry.npm.taobao.org/clean-webpack-plugin/download/clean-webpack-plugin-3.0.0.tgz" -r ./index-v5/ ./index-v5//d0/84/e30448aecdc817bb6a8706d91392......(省略)
在
./index-v5//d0/84/
找到依赖包索引文件,格式化 json 得:{ "key": "pacote:tag-manifest:https://registry.npm.taobao.org/clean-webpack-plugin/download/clean-webpack-plugin-3.0.0.tgz:sha1-qZ2Ow0wcYopFQVZ6p7RXRGRgxis=", "integrity": "sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==", "time": 1598713645175, "size": 1, "metadata": { "id": "clean-webpack-plugin@3.0.0", "manifest": { "name": "clean-webpack-plugin", "version": "3.0.0", "engines": { "node": ">=8.9.0" }, "dependencies": { //... }, "devDependencies": { //... }, "bundleDependencies": false, "peerDependencies": { "webpack": "*" }, "deprecated": false, "_resolved": "https://registry.npm.taobao.org/clean-webpack-plugin/download/clean-webpack-plugin-3.0.0.tgz", "_integrity": "sha1-qZ2Ow0wcYopFQVZ6p7RXRGRgxis=", "_shasum": "a99d8ec34c1c628a4541567aa7b457446460c62b", "_shrinkwrap": null, "bin": null, "_id": "clean-webpack-plugin@3.0.0" }, "type": "finalized-manifest" } }
其中
_shasum
属性a99d8ec34c1c628a4541567aa7b457446460c62b
即为tar
依赖包的hash
,hash
的前几位a99d8
即为缓存的前两层目录。根据
index-v5
索引文件中得到的目录路径,就可以在content-v2
中找到对应的压缩后的依赖包:
以上的缓存策略是从 npm v5 版本开始的,在 npm v5 版本之前,每个缓存的模块在 ~/.npm 文件夹中以模块名的形式直接存储,储存结构是{cache}/{name}/{version}。
# 如何生成缓存
npm 本身只提供清除缓存和验证缓存完整性的方法,不提供直接操作缓存的方法,可以通过 npm cache
来操作这些缓存数据。
npm 是如何对依赖包进行本地化缓存的,即_content_v2
和index-v5
的缓存资源又是如何生成的?
上官方文档链接:https://docs.npmjs.com/cli/v8/commands/npm-cache#details
大概意思如下:npm 会将所有的缓存数据存储在已配置的 cache 路径下,命名为_cacache
的目录中。_cache
存这个目录主要是通过 pacote
去获取到缓存资源,它存储所有的 http 请求数据以及其他包相关的数据。缓存的资源在插入和提取时都会经过完整验证,如果有损坏将会触发错误或者触发pacote
,自动进行重新获取数据的信号。因此,除非是需要回收磁盘空间,缓存文件没必要进行清除,npm 也不会自行删除数据,当我们npm clean cache
时需要--force
运行。
也由此可知,npm 主要是用 pacote (opens new window) 来处理依赖包的。那什么是 pacote ?pacote 是 npm 用来下载依赖包元数据的模块,大概的思路是结合网络请求和文件读写配置进行依赖包资源的写入和生成对应的压缩文件。
npm 主要有以下三个地方会用到 pacote:
当
npm install xx
执行的时候,如果存在缓存时,将会通过 pacote.extract 把缓存包解压到对应的 node_modules 下面当
npm install xx
执行时,不存在缓存,将会先进行资源下载和完整性校验,接着运行npm cache add xx
,通过pacote.tarball.stream
往.npm/_cacache
里增加缓存数据npm pack xxx
会通过pacote.tarball.toFile
在当前路径生成对应的压缩文件
而 pacote 又是依赖 npm-registry-fetch
来下载包,在给指定路径下根据 IETF RFC 7234 (opens new window) 生成缓存数据。
# npmrc 是啥
.npmrc,可以理解成npm running cnfiguration
, 即 npm 运行时配置文件。我们知道,npm 最大的作用就是帮助开发者安装需要的依赖包,但是要从哪里下载?下载哪一个版本的包,把包下载到电脑的哪个路径下?这些都可以在.npmrc 中进行配置。
# npmrc 是有权重的
npm 按照如下顺序读取这些配置文件:
项目级的.npmrc 文件
可以在项目的根目录下创建一个.npmrc 文件,只用于管理这个项目的 npm 安装用户级的 .npmrc 文件
在你使用一个账号登陆的电脑的时候,可以为当前用户创建一个.npmrc 文件,之后用该用户登录电脑,就可以使用该配置文件。可以通过npm config get userconfig
来获取该文件的位置全局级的 .npmrc
一台电脑可能有多个用户,在这些用户之上,你可以设置一个公共的.npmrc
文件,供所有用户使用。该文件的路径为:$PREFIX/etc/npmrc
,使用npm config get prefix
获取\$PREFIX
。如果你不曾配置过全局文件,该文件不存在。npm 内置的 .npmrc
基本上用不到,不用过度关注。